#!/bin/bash
# MySQL 8.0 InnoDB Redo Log Analyzer
# 基于源码分析编写的redo log查看工具

set -e

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color

# 常量定义 (基于 log0constants.h)
OS_FILE_LOG_BLOCK_SIZE=512
LOG_BLOCK_HDR_SIZE=12
LOG_BLOCK_TRL_SIZE=4
LOG_BLOCK_DATA_SIZE=496
LOG_FILE_HDR_SIZE=2048  # 4 * OS_FILE_LOG_BLOCK_SIZE
LOG_CHECKPOINT_1=512
LOG_CHECKPOINT_2=1536
LOG_ENCRYPTION=1024

# 偏移量定义
LOG_BLOCK_HDR_NO=0
LOG_BLOCK_HDR_DATA_LEN=4
LOG_BLOCK_FIRST_REC_GROUP=6
LOG_BLOCK_EPOCH_NO=8
LOG_BLOCK_CHECKSUM=4

# 函数：显示帮助信息
show_help() {
    echo -e "${CYAN}MySQL 8.0 InnoDB Redo Log Analyzer${NC}"
    echo ""
    echo "用法: $0 [选项] <redo-log-file>"
    echo ""
    echo "选项:"
    echo "  -h, --help          显示此帮助信息"
    echo "  -i, --info          显示文件基本信息"
    echo "  -H, --header        显示文件头部信息"
    echo "  -c, --checkpoint    显示检查点信息"
    echo "  -b, --blocks        显示日志块信息"
    echo "  -d, --data          显示数据内容(十六进制)"
    echo "  -s, --summary       显示摘要信息"
    echo "  -a, --all           显示所有信息"
    echo "  --start-block N     从第N个块开始显示"
    echo "  --max-blocks N      最多显示N个块"
    echo "  --datadir PATH      指定MySQL数据目录"
    echo ""
    echo "示例:"
    echo "  $0 -i ib_redo0           # 显示基本信息"
    echo "  $0 -a ib_redo0           # 显示所有信息"
    echo "  $0 -b --max-blocks 10 ib_redo0  # 显示前10个块"
    echo "  $0 --datadir /var/lib/mysql/#innodb_redo -s ib_redo0"
}

# 函数：检查工具可用性
check_tools() {
    local missing_tools=()
    
    for tool in hexdump od xxd; do
        if ! command -v "$tool" >/dev/null 2>&1; then
            missing_tools+=("$tool")
        fi
    done
    
    if [ ${#missing_tools[@]} -gt 0 ]; then
        echo -e "${RED}错误: 缺少必要工具: ${missing_tools[*]}${NC}" >&2
        echo "请安装: apt-get install bsdmainutils" >&2
        exit 1
    fi
}

# 函数：读取字节
read_bytes() {
    local file="$1"
    local offset="$2"
    local length="$3"
    local format="${4:-x}"
    
    if [ "$format" = "d" ]; then
        # 十进制格式
        od -An -td1 -j "$offset" -N "$length" "$file" | tr -d ' \n'
    else
        # 十六进制格式
        hexdump -s "$offset" -n "$length" -e "\"$length/1 \"%02x\" \"\"" "$file"
    fi
}

# 函数：读取4字节整数(小端序)
read_uint32_le() {
    local file="$1"
    local offset="$2"
    
    local bytes=$(read_bytes "$file" "$offset" 4)
    local b1=$(echo "$bytes" | cut -c1-2)
    local b2=$(echo "$bytes" | cut -c3-4)
    local b3=$(echo "$bytes" | cut -c5-6)
    local b4=$(echo "$bytes" | cut -c7-8)
    
    echo $((0x$b1 + 0x$b2*256 + 0x$b3*65536 + 0x$b4*16777216))
}

# 函数：读取8字节整数(小端序)
read_uint64_le() {
    local file="$1"
    local offset="$2"
    
    local low=$(read_uint32_le "$file" "$offset")
    local high=$(read_uint32_le "$file" $((offset + 4)))
    
    echo $((low + high * 4294967296))
}

# 函数：显示文件信息
show_file_info() {
    local file="$1"
    
    if [ ! -f "$file" ]; then
        echo -e "${RED}错误: 文件 '$file' 不存在${NC}" >&2
        return 1
    fi
    
    local size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null)
    local blocks=$((size / OS_FILE_LOG_BLOCK_SIZE))
    local data_blocks=$((blocks - 4))  # 减去文件头的4个块
    
    echo -e "${GREEN}=== Redo Log 文件信息 ===${NC}"
    echo "文件名: $file"
    echo "文件大小: $size 字节 ($(printf "%.2f" $(echo "scale=2; $size/1024/1024" | bc)) MB)"
    echo "日志块数: $blocks 个"
    echo "数据块数: $data_blocks 个"
    echo "块大小: $OS_FILE_LOG_BLOCK_SIZE 字节"
    echo "头部大小: $LOG_BLOCK_HDR_SIZE 字节"
    echo "尾部大小: $LOG_BLOCK_TRL_SIZE 字节"
    echo "数据区大小: $LOG_BLOCK_DATA_SIZE 字节/块"
}

# 函数：显示文件头信息
show_file_header() {
    local file="$1"
    
    echo -e "${GREEN}=== Redo Log 文件头信息 ===${NC}"
    
    # 第一个块 - 文件头
    echo -e "${YELLOW}--- 文件头块 (0-511字节) ---${NC}"
    local magic=$(read_bytes "$file" 0 4)
    echo "Magic Number: 0x$magic"
    
    # 检查点1
    echo -e "${YELLOW}--- 检查点1 (512-1023字节) ---${NC}"
    local cp1_no=$(read_uint64_le "$file" $((LOG_CHECKPOINT_1 + 0)))
    local cp1_lsn=$(read_uint64_le "$file" $((LOG_CHECKPOINT_1 + 8)))
    echo "检查点号: $cp1_no"
    echo "检查点LSN: $cp1_lsn"
    
    # 加密头
    echo -e "${YELLOW}--- 加密头 (1024-1535字节) ---${NC}"
    local encrypt_info=$(read_bytes "$file" $LOG_ENCRYPTION 16)
    echo "加密信息: 0x$encrypt_info"
    
    # 检查点2
    echo -e "${YELLOW}--- 检查点2 (1536-2047字节) ---${NC}"
    local cp2_no=$(read_uint64_le "$file" $((LOG_CHECKPOINT_2 + 0)))
    local cp2_lsn=$(read_uint64_le "$file" $((LOG_CHECKPOINT_2 + 8)))
    echo "检查点号: $cp2_no"
    echo "检查点LSN: $cp2_lsn"
    
    # 确定当前检查点
    if [ "$cp1_no" -gt "$cp2_no" ]; then
        echo -e "${CYAN}当前检查点: 检查点1 (LSN: $cp1_lsn)${NC}"
    else
        echo -e "${CYAN}当前检查点: 检查点2 (LSN: $cp2_lsn)${NC}"
    fi
}

# 函数：显示日志块信息
show_log_blocks() {
    local file="$1"
    local start_block="${2:-4}"  # 默认从第4个块开始(跳过文件头)
    local max_blocks="${3:-10}"  # 默认显示10个块
    
    local file_size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null)
    local total_blocks=$((file_size / OS_FILE_LOG_BLOCK_SIZE))
    
    echo -e "${GREEN}=== Redo Log 块信息 ===${NC}"
    echo "起始块: $start_block, 最大显示: $max_blocks"
    
    local count=0
    for ((block = start_block; block < total_blocks && count < max_blocks; block++)); do
        local offset=$((block * OS_FILE_LOG_BLOCK_SIZE))
        
        echo -e "${YELLOW}--- 块 #$block (偏移: $offset) ---${NC}"
        
        # 读取块头信息
        local hdr_no=$(read_uint32_le "$file" $((offset + LOG_BLOCK_HDR_NO)))
        local data_len=$(read_uint32_le "$file" $((offset + LOG_BLOCK_HDR_DATA_LEN)))
        local first_rec=$(read_uint32_le "$file" $((offset + LOG_BLOCK_FIRST_REC_GROUP)))
        local epoch_no=$(read_uint32_le "$file" $((offset + LOG_BLOCK_EPOCH_NO)))
        
        # 读取校验和
        local checksum_offset=$((offset + OS_FILE_LOG_BLOCK_SIZE - LOG_BLOCK_CHECKSUM))
        local checksum=$(read_uint32_le "$file" "$checksum_offset")
        
        # 检查加密位
        local is_encrypted=""
        if [ $((data_len & 0x8000)) -ne 0 ]; then
            is_encrypted=" (加密)"
            data_len=$((data_len & 0x7FFF))
        fi
        
        echo "  块号: $hdr_no"
        echo "  数据长度: $data_len$is_encrypted"
        echo "  第一记录组偏移: $first_rec"
        echo "  纪元号: $epoch_no"
        echo "  校验和: 0x$(printf "%08x" $checksum)"
        
        # 显示数据预览
        if [ "$data_len" -gt "$LOG_BLOCK_HDR_SIZE" ]; then
            local actual_data_len=$((data_len - LOG_BLOCK_HDR_SIZE))
            if [ "$actual_data_len" -gt 32 ]; then
                actual_data_len=32
            fi
            local data_preview=$(read_bytes "$file" $((offset + LOG_BLOCK_HDR_SIZE)) "$actual_data_len")
            echo "  数据预览: 0x$data_preview..."
        else
            echo "  数据预览: (空块)"
        fi
        
        count=$((count + 1))
    done
}

# 函数：显示摘要信息
show_summary() {
    local file="$1"
    
    echo -e "${GREEN}=== Redo Log 摘要 ===${NC}"
    
    # 文件大小信息
    local file_size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null)
    local total_blocks=$((file_size / OS_FILE_LOG_BLOCK_SIZE))
    local data_blocks=$((total_blocks - 4))
    
    echo "文件大小: $(printf "%.2f" $(echo "scale=2; $file_size/1024/1024" | bc)) MB"
    echo "总块数: $total_blocks"
    echo "数据块数: $data_blocks"
    
    # 检查点信息
    local cp1_lsn=$(read_uint64_le "$file" $((LOG_CHECKPOINT_1 + 8)))
    local cp2_lsn=$(read_uint64_le "$file" $((LOG_CHECKPOINT_2 + 8)))
    local current_lsn=$cp1_lsn
    if [ "$cp2_lsn" -gt "$cp1_lsn" ]; then
        current_lsn=$cp2_lsn
    fi
    echo "当前LSN: $current_lsn"
    
    # 统计非空块
    local non_empty_blocks=0
    for ((block = 4; block < total_blocks; block++)); do
        local offset=$((block * OS_FILE_LOG_BLOCK_SIZE))
        local data_len=$(read_uint32_le "$file" $((offset + LOG_BLOCK_HDR_DATA_LEN)))
        data_len=$((data_len & 0x7FFF))  # 移除加密位
        
        if [ "$data_len" -gt "$LOG_BLOCK_HDR_SIZE" ]; then
            non_empty_blocks=$((non_empty_blocks + 1))
        fi
    done
    
    echo "非空块数: $non_empty_blocks"
    echo "空块数: $((data_blocks - non_empty_blocks))"
    echo "使用率: $(printf "%.2f" $(echo "scale=2; $non_empty_blocks*100/$data_blocks" | bc))%"
}

# 函数：扫描MySQL数据目录
scan_datadir() {
    local datadir="$1"
    
    if [ -z "$datadir" ]; then
        # 尝试自动检测
        for dir in "/var/lib/mysql/#innodb_redo" "/usr/local/mysql/data/#innodb_redo" "./data/#innodb_redo"; do
            if [ -d "$dir" ]; then
                datadir="$dir"
                break
            fi
        done
    fi
    
    if [ -z "$datadir" ] || [ ! -d "$datadir" ]; then
        echo -e "${RED}错误: 无法找到MySQL数据目录${NC}" >&2
        echo "请使用 --datadir 指定正确的路径" >&2
        return 1
    fi
    
    echo -e "${GREEN}=== MySQL Redo Log 文件扫描 ===${NC}"
    echo "数据目录: $datadir"
    echo ""
    
    local redo_files=($(ls "$datadir"/#ib_redo* 2>/dev/null || true))
    
    if [ ${#redo_files[@]} -eq 0 ]; then
        echo -e "${YELLOW}未找到redo log文件${NC}"
        return 0
    fi
    
    for file in "${redo_files[@]}"; do
        local basename=$(basename "$file")
        local size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file" 2>/dev/null)
        local size_mb=$(printf "%.2f" $(echo "scale=2; $size/1024/1024" | bc))
        
        echo -e "${CYAN}文件: $basename${NC} (${size_mb}MB)"
        
        # 显示简要信息
        local cp1_lsn=$(read_uint64_le "$file" $((LOG_CHECKPOINT_1 + 8)))
        local cp2_lsn=$(read_uint64_le "$file" $((LOG_CHECKPOINT_2 + 8)))
        local current_lsn=$cp1_lsn
        if [ "$cp2_lsn" -gt "$cp1_lsn" ]; then
            current_lsn=$cp2_lsn
        fi
        echo "  当前LSN: $current_lsn"
        echo ""
    done
}

# 主函数
main() {
    local show_info=false
    local show_header=false
    local show_checkpoint=false
    local show_blocks=false
    local show_data=false
    local show_summary=false
    local show_all=false
    local start_block=4
    local max_blocks=10
    local datadir=""
    local filename=""
    
    # 参数解析
    while [[ $# -gt 0 ]]; do
        case $1 in
            -h|--help)
                show_help
                exit 0
                ;;
            -i|--info)
                show_info=true
                shift
                ;;
            -H|--header)
                show_header=true
                shift
                ;;
            -c|--checkpoint)
                show_checkpoint=true
                shift
                ;;
            -b|--blocks)
                show_blocks=true
                shift
                ;;
            -d|--data)
                show_data=true
                shift
                ;;
            -s|--summary)
                show_summary=true
                shift
                ;;
            -a|--all)
                show_all=true
                shift
                ;;
            --start-block)
                start_block="$2"
                shift 2
                ;;
            --max-blocks)
                max_blocks="$2"
                shift 2
                ;;
            --datadir)
                datadir="$2"
                shift 2
                ;;
            -*)
                echo -e "${RED}错误: 未知选项 '$1'${NC}" >&2
                echo "使用 -h 查看帮助信息" >&2
                exit 1
                ;;
            *)
                filename="$1"
                shift
                ;;
        esac
    done
    
    # 检查工具
    check_tools
    
    # 如果指定了数据目录但没有文件名，扫描目录
    if [ -n "$datadir" ] && [ -z "$filename" ]; then
        scan_datadir "$datadir"
        exit 0
    fi
    
    # 检查文件名
    if [ -z "$filename" ]; then
        echo -e "${RED}错误: 请指定redo log文件${NC}" >&2
        echo "使用 -h 查看帮助信息" >&2
        exit 1
    fi
    
    # 如果指定了数据目录，构造完整路径
    if [ -n "$datadir" ]; then
        filename="$datadir/$filename"
    fi
    
    # 默认显示摘要
    if ! $show_info && ! $show_header && ! $show_checkpoint && ! $show_blocks && ! $show_data && ! $show_summary && ! $show_all; then
        show_summary=true
    fi
    
    # 显示信息
    if $show_all; then
        show_file_info "$filename"
        echo ""
        show_file_header "$filename"
        echo ""
        show_log_blocks "$filename" "$start_block" "$max_blocks"
        echo ""
        show_summary "$filename"
    else
        if $show_info; then
            show_file_info "$filename"
            echo ""
        fi
        
        if $show_header || $show_checkpoint; then
            show_file_header "$filename"
            echo ""
        fi
        
        if $show_blocks; then
            show_log_blocks "$filename" "$start_block" "$max_blocks"
            echo ""
        fi
        
        if $show_summary; then
            show_summary "$filename"
        fi
    fi
}

# 运行主函数
main "$@"